/*******************************************************************************
 * Copyright (c) 2010 Mikhail Garber.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser Public License (LGPL)
 * which accompanies this distribution
 * 
 * Contributors:
 *     Mikhail Garber - initial API and implementation
 ******************************************************************************/
package plumber.annotations;

import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import plumber.common.HasSetup;
import plumber.flow.FlowLocator;
import plumber.model.ClassBasedOutcome;
import plumber.model.DefaultOutcome;
import plumber.model.ExceptionOutcome;
import plumber.model.Flow;
import plumber.model.FlowStep;
import plumber.model.StepOutcome;
import plumber.model.ValueOutcome;

/**
 * a Flow locator that can, at startup time, build list of Flows based on source-code annotations
 *  
 * @author mgarber
 *
 */
public class AnnotatedFlowLocator implements FlowLocator, HasSetup {

	private String[] targetClasses;
	private Set<Flow> flows;
	private Logger logger = LoggerFactory.getLogger(AnnotatedFlowLocator.class);
	
	/**
	 * classes to inspect for Flow annotations
	 * @param targetClasses
	 */
	public void setTargetClasses(String[] targetClasses) {
		this.targetClasses = targetClasses;
	}


	@Override
	public Set<Flow> getFlows() {
		return flows;
	}


	@SuppressWarnings("rawtypes")
	@Override
	/**
	 * runs on startup
	 */
	public void setup() {
		flows = new HashSet<Flow>();
		
		for(String cName : targetClasses) {
			try {
				logger.info("class:" + cName);
				Class z = Class.forName(cName);
				for(Method m : z.getMethods()) {
					logger.info("method:" + m);
					if(m.isAnnotationPresent(Plumber.class)) {
						Plumber an = m.getAnnotation(Plumber.class);
						for(PlumberFlow f : an.flows()) {
							setupAnnotation(cName, m, f);
						}
					}
					if(m.isAnnotationPresent(PlumberFlow.class)) {
						
						PlumberFlow an = m.getAnnotation(PlumberFlow.class);
						setupAnnotation(cName, m, an);
					}
				}
			} catch(Exception ex) {
				logger.warn("working with cname:" + cName, ex);
			}
		}
		logger.info("setup flows:" + flows);
	}


	private void setupAnnotation(String cName, Method m, PlumberFlow an)
			throws ClassNotFoundException {
		logger.info("setting up");
		Flow flow = getOrCreate(an.flow());
		FlowStep step = getOrCreate(flow, an.step());
		step.setComponent(cName);
		step.setMaxRetryCount(an.maxRetryCount());
		step.setMaxTimeSeconds(an.maxRuntimeSeconds());
		step.setMethod(m.getName());
		if(an.firstStep()) {
			flow.setFirstStep(step);
		}
		Set<StepOutcome> outcomes = new HashSet<StepOutcome>();
		for(Outcome oc : an.outcomes()) {
			if(! "".equals(oc.ifValue())) {
				ValueOutcome valueOutcome = new ValueOutcome();
				valueOutcome.setNextStep(getOrCreate(flow, oc.nextStep()));
				valueOutcome.setValue(oc.ifValue());
				outcomes.add(valueOutcome);
			} else if( ! "".equals(oc.ifClassName())) {
				ClassBasedOutcome outcome = new ClassBasedOutcome();
				outcome.setNextStep(getOrCreate(flow, oc.nextStep()));
				outcome.setTriggerClass(Class.forName(oc.ifClassName()));
				outcomes.add(outcome);
			} else if ( ! "".equals(oc.ifExceptionClassName())) {
				ExceptionOutcome outcome = new ExceptionOutcome();
				outcome.setNextStep(getOrCreate(flow, oc.nextStep()));
				outcome.setTriggerClass(Class.forName(oc.ifExceptionClassName()));
				outcomes.add(outcome);
			} else {
				DefaultOutcome outcome = new DefaultOutcome();
				outcome.setNextStep(getOrCreate(flow, oc.nextStep()));
				outcomes.add(outcome);
			}
		}
		step.setOutcomes(outcomes);
	}


	private Flow getOrCreate(String flowName) {
		for(Flow flow : flows) {
			if(flow.getName().equals(flowName)) {
				return flow;
			}
		}
		Flow flow = new Flow();
		flow.setName(flowName);
		flows.add(flow);
		return flow;
	}

	private FlowStep getOrCreate(Flow flow, String stepName) {

		Set<FlowStep> steps = flow.getSteps();
		if(steps == null) {
			steps = new HashSet<FlowStep>();
			flow.setSteps(steps);
		}
		for(FlowStep step : steps) {
			if(step.getId().equals(stepName)) {
				return step;
			}
		}
		FlowStep step = new FlowStep();
		step.setId(stepName);
		HashSet<FlowStep> newSteps = new HashSet<FlowStep>(steps);
		newSteps.add(step);
		flow.setSteps(newSteps);
		return step;
	}

	@Override
	public void shutdown() {
		
	}

}
